"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { MessageList } from "@/components/dashboard/MessageList"; import { MessageInput } from "@/components/dashboard/MessageInput"; import { ChatHeader } from "@/components/dashboard/ChatHeader"; import { Button } from "@/components/ui/button"; import { useAuth } from "@/features/agent/hooks/useAuth"; import { useWebSocket } from "@/features/agent/hooks/useWebSocket"; import { fetchMessages, sendMessage as sendMessageApi, markMessagesRead, } from "@/features/agent/services/messageApi"; import { fetchConversationDetail } from "@/features/agent/services/conversationApi"; import { ConversationDetail, ConversationSummary, MessageItem, MessagesReadPayload, ChatWebSocketPayload, } from "@/features/agent/types"; import type { WSMessage } from "@/lib/websocket"; import { toast } from "@/hooks/useToast"; import { getAgentWSToken } from "@/utils/storage"; export default function AgentChatPage() { const params = useParams(); const router = useRouter(); const { agent, loading: authLoading } = useAuth(); const conversationId = useMemo(() => { const id = params?.conversationId; if (!id) { return null; } const parsed = Number.parseInt(String(id), 10); return Number.isNaN(parsed) ? null : parsed; }, [params]); const [messages, setMessages] = useState([]); const [conversationDetail, setConversationDetail] = useState(null); const [conversationSummary, setConversationSummary] = useState(null); const [messageInput, setMessageInput] = useState(""); const [loadingMessages, setLoadingMessages] = useState(true); const [sending, setSending] = useState(false); const [highlightKeyword, setHighlightKeyword] = useState(""); const wsToken = getAgentWSToken() ?? undefined; const handleMarkMessagesRead = useCallback( async ( conversationIdParam?: number, readerIsAgentParam?: boolean ) => { const targetConversationId = conversationIdParam ?? conversationId; const targetReaderIsAgent = readerIsAgentParam ?? true; if (!targetConversationId) { return; } const result = await markMessagesRead( targetConversationId, targetReaderIsAgent ); if (!result || result.message_ids.length === 0) { return; } const idSet = new Set(result.message_ids); setMessages((prev) => prev.map((msg) => idSet.has(msg.id) ? { ...msg, is_read: true, read_at: result.read_at ?? msg.read_at ?? null, } : msg ) ); if (targetReaderIsAgent) { setConversationDetail((prev) => prev ? { ...prev, unread_count: result.unread_count } : prev ); setConversationSummary((prev) => prev ? { ...prev, unread_count: result.unread_count } : prev ); } else { setConversationDetail((prev) => prev ? { ...prev, last_seen_at: result.read_at ?? prev.last_seen_at ?? null, } : prev ); } }, [conversationId] ); const loadConversationDetail = useCallback(async () => { if (!conversationId) { return; } const detail = await fetchConversationDetail(conversationId); if (detail) { setConversationDetail(detail); setConversationSummary({ id: detail.id, visitor_id: detail.visitor_id, agent_id: detail.agent_id, status: detail.status, created_at: detail.created_at, updated_at: detail.updated_at, last_message: detail.last_message, unread_count: detail.unread_count ?? 0, }); } }, [conversationId]); const loadMessages = useCallback(async () => { if (!conversationId) { return; } setLoadingMessages(true); try { const data = await fetchMessages(conversationId); setMessages(data); // 注意:不再自动标记访客消息为已读,而是通过滚动检测来处理 } catch (error) { console.error("拉取消息失败:", error); } finally { setLoadingMessages(false); } }, [conversationId]); useEffect(() => { if (!conversationId) { router.push("/agent/dashboard"); } }, [conversationId, router]); useEffect(() => { if (!conversationId || !agent) { return; } loadConversationDetail(); loadMessages(); }, [conversationId, agent, loadConversationDetail, loadMessages]); const handleNewMessage = useCallback( (message: MessageItem) => { setMessages((prev) => { const exists = prev.some((item) => item.id === message.id); if (exists) { return prev; } return [...prev, message]; }); setConversationDetail((prev) => { if (!prev) { return prev; } const nextUnread = !message.sender_is_agent && message.message_type !== "system_message" ? (prev.unread_count ?? 0) + 1 : prev.unread_count ?? 0; return { ...prev, updated_at: message.created_at, unread_count: message.sender_is_agent || message.message_type === "system_message" ? prev.unread_count ?? 0 : nextUnread, last_message: { id: message.id, content: message.content, sender_is_agent: message.sender_is_agent, message_type: message.message_type ?? "user_message", is_read: Boolean(message.is_read), read_at: message.read_at ?? null, created_at: message.created_at, }, }; }); setConversationSummary((prev) => { if (!prev) { return prev; } const nextUnread = !message.sender_is_agent && message.message_type !== "system_message" ? (prev.unread_count ?? 0) + 1 : prev.unread_count ?? 0; return { ...prev, updated_at: message.created_at, unread_count: message.sender_is_agent || message.message_type === "system_message" ? prev.unread_count ?? 0 : nextUnread, last_message: { id: message.id, content: message.content, sender_is_agent: message.sender_is_agent, message_type: message.message_type ?? "user_message", is_read: Boolean(message.is_read), read_at: message.read_at ?? null, created_at: message.created_at, }, }; }); // 注意:不再自动标记访客消息为已读,而是通过滚动检测来处理 if (message.conversation_id === conversationId) { loadConversationDetail(); } }, [conversationId, loadConversationDetail] ); const handleSendMessage = useCallback(async () => { if (!conversationId || !agent?.id || !messageInput.trim() || sending) { return; } setSending(true); try { const created = await sendMessageApi({ conversationId, content: messageInput, senderId: agent.id, }); setMessageInput(""); if (created) { handleNewMessage(created); } else { await loadMessages(); } } catch (error) { console.error(error); toast.error((error as Error).message); } finally { setSending(false); } }, [agent?.id, conversationId, messageInput, sending, handleNewMessage, loadMessages]); const handleMessagesReadEvent = useCallback( (payload: MessagesReadPayload) => { if (!conversationId) { return; } // 检查对话ID是否匹配 const payloadConversationId = payload?.conversation_id; if (payloadConversationId && payloadConversationId !== conversationId) { return; } const ids = Array.isArray(payload?.message_ids) ? payload.message_ids : []; if (ids.length === 0) { return; } const readAt = payload?.read_at; const readerIsAgent = Boolean(payload?.reader_is_agent); const unreadCount = typeof payload?.unread_count === "number" ? payload.unread_count : undefined; // 对于客服端:只有当 reader_is_agent === false 时(访客读取了客服的消息), // 才更新客服消息(sender_is_agent === true)的已读状态 if (readerIsAgent) { // 客服读取了访客的消息,更新未读数(不更新消息的已读状态,因为这是客服读取的) if (unreadCount !== undefined) { setConversationDetail((prev) => prev ? { ...prev, unread_count: unreadCount } : prev ); setConversationSummary((prev) => prev ? { ...prev, unread_count: unreadCount } : prev ); } return; } // 访客读取了客服的消息,更新客服消息的已读状态 const messageIdSet = new Set(ids); setMessages((prev) => prev.map((msg) => // 只更新客服自己的消息(sender_is_agent === true)的已读状态 messageIdSet.has(msg.id) && msg.sender_is_agent ? { ...msg, is_read: true, read_at: readAt ?? msg.read_at ?? null, } : msg ) ); // 更新访客的最后活跃时间 setConversationDetail((prev) => prev ? { ...prev, last_seen_at: readAt ?? prev.last_seen_at ?? null } : prev ); }, [conversationId] ); const handleWebSocketMessage = useCallback( (event: WSMessage) => { if (!event) { return; } if (event.type === "new_message" && event.data) { const data = event.data as MessageItem; if (typeof data.conversation_id === "number") { handleNewMessage(data); } } else if (event.type === "messages_read") { // 确保处理已读事件,并传入对话ID const payload = event.data as MessagesReadPayload; if (!payload.conversation_id && event.conversation_id) { payload.conversation_id = event.conversation_id; } handleMessagesReadEvent(payload); } }, [handleMessagesReadEvent, handleNewMessage] ); useWebSocket({ conversationId, enabled: Boolean(conversationId), isVisitor: false, agentId: agent?.id ?? undefined, wsToken, onMessage: handleWebSocketMessage, onError: (error) => { // 静默处理错误,避免影响用户体验 }, onClose: () => { // 静默处理关闭,避免影响用户体验 }, }); const handleBack = useCallback(() => { router.push("/agent/dashboard"); }, [router]); const unreadCount = conversationDetail?.unread_count ?? conversationSummary?.unread_count ?? 0; if (authLoading || !agent) { return (
加载中...
); } if (!conversationId) { return null; } return (
handleMarkMessagesRead(conversationId, true)} onRefresh={() => { loadMessages(); loadConversationDetail(); }} />
setHighlightKeyword("")} currentUserIsAgent={true} conversationId={conversationId} onMarkMessagesRead={handleMarkMessagesRead} />
); }